package serverside;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Scanner;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManagerFactory;

public class Server implements Runnable {

	private KeyStore clientKeyStore;
	private KeyStore serverKeyStore;
	private SSLContext sc;
	private String passphrase = "secret";
	static private SecureRandom rnd;

	private int port = 50000;
	private DataInputStream in;
	private DataOutputStream out;
	static private String pathPW;
	static private String pathDB;
	private RecordDatabase db;

	private static final int EXIT = 0;
	private static final int PATIENTLISTING = 1;
	private static final int DIVISIONLISTING = 2;
	private static final int NURSELISTING = 3;
	private static final int SPEC_NURSELISTING = 4;
	private static final int LISTALL = 5;
	private static final int SPEC_LISTALL = 6;
	private static final int APPEND_RECORD = 7;
	private static final int DELETE_RECORDS = 8;
	private static final int SPEC_DELETE_RECORD = 9;
	private static final int ATTEMPT_LIMIT = 5;

	public Server() {
		db	= new RecordDatabase(pathDB);
		new Thread(this).run();
	}

	private void setupClientKeyStore() throws GeneralSecurityException, IOException {
		clientKeyStore = KeyStore.getInstance("JKS");
		clientKeyStore.load(new FileInputStream("client.public"), "public".toCharArray());
	}

	private void setupServerKeystore() throws GeneralSecurityException, IOException {
		serverKeyStore = KeyStore.getInstance("JKS");
		serverKeyStore.load(new FileInputStream("server.private"), passphrase.toCharArray());
	}

	private void setupSSLContext() throws GeneralSecurityException, IOException {
		TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
		tmf.init(clientKeyStore);

		KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
		kmf.init(serverKeyStore, passphrase.toCharArray());

		sc = SSLContext.getInstance("SSL");
		sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), rnd);
	}

	public void run() {
		String result = "";
		String name = "";
		String patient = "";
		String div = "";
		Socket socket = null;
		try {
			setupClientKeyStore();
			setupServerKeystore();
			setupSSLContext();

			SSLServerSocketFactory sf = sc.getServerSocketFactory();
			SSLServerSocket ss = (SSLServerSocket)sf.createServerSocket(port);
			ss.setNeedClientAuth(true);
			while(true) {
				System.out.println("Waiting for client on port: " + port);
				socket = ss.accept();
				
				System.out.println("Got connection from " + socket);

				in = new DataInputStream(socket.getInputStream());
				out = new DataOutputStream(socket.getOutputStream());
				int authority = -1;
				String username = "";
				while(authority == -1) {
					username = in.readUTF();
					String pw = in.readUTF();

					authority = loginUser(username, pw);

					out.writeInt(authority);
					out.flush();
				}
				System.out.println(username + " logged in with authority: " + authority);

				boolean run = true;
				while(run) {
					System.out.println("Awaiting command...");
					int action = in.readInt();
					System.out.println("Command: " + action);
					switch(action) {
					case EXIT:
						out.writeBoolean(true);
						out.flush();
						run = false;
						break;
					case PATIENTLISTING:
						if(authority != 1) {
							out.writeBoolean(false);
							out.flush();
							continue;
						}
						name = getName(username);
						sendResult(db.getRecords(db.PATIENT, name));
						break;
					case DIVISIONLISTING:
						if(authority != 2) {
							out.writeBoolean(false);
							out.flush();
							continue;
						}
						div = getDivision(username);
						sendResult(db.getRecords(db.DIVISION, div));
						break;
					case NURSELISTING:
						if(authority != 2) {
							out.writeBoolean(false);
							out.flush();
							continue;
						}
						name = getName(username);
						sendResult(db.getRecords(db.NURSE, name));
						break;
					case SPEC_NURSELISTING:
						out.writeBoolean(authority==2);
						out.flush();
						if(authority != 2)
							continue;
						patient = in.readUTF();
						name = getName(username);
						div = getDivision(username);
						result = db.getRecords(patient, name, div);
						sendResult(result);
						break;
					case LISTALL:
						if(authority != 3 && authority != 4){
							out.writeBoolean(false);
							out.flush();
							continue;
						}
						sendResult(db.getAllRecords());
						break;
					case SPEC_LISTALL:
						out.writeBoolean(authority==3 || authority==4);
						out.flush();
						if(authority!=3 && authority!=4)
							continue;
						patient = in.readUTF();
						result = db.getRecords(db.PATIENT, patient);
						sendResult(result);
						break;
					case APPEND_RECORD:
						out.writeBoolean(authority==3);
						out.flush();
						if(authority!=3)
							continue;
						patient = in.readUTF();
						String doctor = in.readUTF();
						String nurse = in.readUTF();
						div = in.readUTF();
						String comment = in.readUTF();
						out.writeBoolean(db.addRecord(patient, doctor, nurse, div, comment));
						out.flush();
						break;
					case SPEC_DELETE_RECORD:
						out.writeBoolean(authority==4);
						out.flush();
						if(authority != 4){
							continue;
						}
						patient = in.readUTF();
						int rec = in.readInt();
						out.writeBoolean(db.removeRecord(rec, patient));
						out.flush();
						break;
					case DELETE_RECORDS:
						out.writeBoolean(authority==4);
						out.flush();
						if(authority != 4){
							continue;
						}
						patient = in.readUTF();
						out.writeBoolean(db.removeRecord(-1, patient));
						out.flush();
						break;
					}

				}
				System.out.println(username + " logged off.");
				socket.close();
			}
		} catch (GeneralSecurityException gse) {
			gse.printStackTrace();
		} catch (IOException ioe) {
			if(socket != null) {
				try {
					socket.close();
				} catch( IOException ie2 ) {
					System.out.println( "Error closing socket "+socket );
				}

				System.out.println( "Closed connection from socket "+socket );
			} else {
				ioe.printStackTrace();
			}
		}
	}

	private void sendResult(String result) throws IOException {
		out.writeBoolean(!result.equals(""));
		out.writeUTF(result);
		out.flush();
	}


	private String getColumn(int col, String username) { 
		File file = new File(pathPW );
		Scanner sc = null;
		String name = "";
		try {
			sc = new Scanner(file);
		} catch (FileNotFoundException e) {
			System.err.println("Password file missing at: " + pathPW);
			return name;
		}

		while(sc.hasNext()) {
			String line = sc.nextLine();

			if(line.trim().isEmpty()) {
				continue;
			}

			String[] data = line.split("; ");
			if(data[0].toLowerCase().equals(username.toLowerCase())) {
				name = data[col];
				break;
			}
		}
		sc.close();
		return name;
	}

	private String getName(String username) {
		return getColumn(3, username);
	}

	private String getDivision(String username) {
		return getColumn(4, username);
	}

	private int loginUser(String username, String pw) {

		File file = new File(pathPW );
		Scanner sc = null;
		int authority = -1;
		try {
			sc = new Scanner(file);
		} catch (FileNotFoundException e) {
			System.err.println("Password file missing at: " + pathPW);
			return -1;
		}
		ArrayList<String> accounts = new ArrayList<String>();
		while(sc.hasNext()) {
			String line = sc.nextLine();

			if(line.trim().isEmpty()) {
				continue;
			}

			accounts.add(line);
		}

		sc.close();

		for(int i = 0; i < accounts.size(); i++) {
			String[] data = accounts.get(i).split("; ");
			int attempts = Integer.parseInt(data[5]);
			if(data[0].toLowerCase().equals(username.toLowerCase())
					&& data[1].equals(pw) && attempts <= ATTEMPT_LIMIT) {
				authority = Integer.parseInt(data[2]);
				data[5] = "0";
				String entry = data[0] + "; " + data[1] + "; " + data[2] + "; "
					+ data[3] + "; " + data[4] + "; " + data[5];
				accounts.remove(i);
				accounts.add(i,entry);
				break;
			} else if(data[0].toLowerCase().equals(username.toLowerCase())
					&& attempts <= ATTEMPT_LIMIT) {
				data[5] = new Integer(attempts + 1).toString();
				String entry = data[0] + "; " + data[1] + "; " + data[2] + "; "
					+ data[3] + "; " + data[4] + "; " + data[5];
				accounts.remove(i);
				accounts.add(i,entry);
				break;
			}
		}
		StringBuilder sb = new StringBuilder();
		ListIterator<String> iter = accounts.listIterator();
		while(iter.hasNext()) {
			sb.append(iter.next() + "\n");
		}

		PrintWriter outfile = null; 
		try {
			outfile = new PrintWriter(new FileWriter(pathPW, false));
		} catch (IOException e) {
			System.err.println("PW not found at: " + pathPW);
			System.exit(1);
		}
		outfile.write(sb.toString());
		outfile.flush();
		outfile.close();

		return authority;
	}

	static public void main( String args[] ) {

		if(args.length < 2) {
			System.out.println("USAGE: <path to database> <path to password file>");
			System.exit(1);
		}

		pathDB = args[0];
		pathPW = args[1];

		System.out.println( "Wait while secure random numbers are initialized...." );
		rnd = new SecureRandom();
		rnd.nextInt();
		System.out.println( "Done." );

		new Server();
	}
}
